#
# A replacement for heat - heat is bugy as it generates duplicate file-ids.
#

abort(".NET Framework is required.") unless defined?(System::Guid)

require 'digest'
require 'stringio'

class Harvester
  def harvest(dir_or_files, wxi_path, generator_name, root)
    puts "#{wxi_path} <- #{dir_or_files.kind_of?(String) ? dir_or_files : '[...]'}"
    load_component_guids wxi_path
    
    @root = root
    @name = file_name_without_extension wxi_path
    @indent = 0
    
    @out = StringIO.new
    out %{<?xml version="1.0" encoding="utf-8"?>}
    out %{<!-- Generated by #{generator_name} -->}
    out %{<Include Id="LibsInclude_#@name">}
    indent
    if dir_or_files.kind_of? String
      generate_from_file_system(dir_or_files)
    else
      generate_from_tree("", build_dir_tree(dir_or_files))
    end
    unindent
    out %{</Include>}
    new_content = @out.string
    
    puts

    # update the file only if it changed
    unless File.exists?(wxi_path) and File.open(wxi_path, "r") { |f| f.read } == new_content
      tf_edit wxi_path
      File.open(wxi_path, "w") { |f| f.write new_content }
    else
      puts "File is up to date."
    end
  end

  private # =================================================
  
  # TODO: merge with generate_from_tree
  def generate_from_file_system dir
    print '.'
    full_path = File.join(ENV["DLR_ROOT"], dir)
    
    dirs = []
    files = [] 
  
    Dir.foreach(full_path) do |entry|
      next if entry == '.' or entry == '..'
      
      if File.directory? File.join(full_path, entry)
        dirs << entry
      else
        files << entry
      end    
    end
  
    dirs.sort.each do |entry|
      entry_relative_path = File.join(dir, entry)
      write_directory entry_relative_path, entry do
        generate_from_file_system entry_relative_path
      end
    end
    
    unless files.empty?
      write_component dir do
        files.sort.each do |entry|
          entry_relative_path = File.join(dir, entry)
          out %{<File Id="F#{make_id(entry_relative_path)}" Source="#{@root}#{entry_relative_path.gsub('/', '\\')}" />}
        end
      end
    end
  end

  def generate_from_tree dir, node
    print '.'

    files = node["."] || []
    dirs = node.keys
    
    dirs.sort.each do |entry|
      next if entry == "." 
      entry_relative_path = File.join(dir, entry)
      
      write_directory entry_relative_path, entry do
        generate_from_tree entry_relative_path, node[entry]
      end
    end
    
    unless files.empty?
      write_component dir do
        files.sort.each do |entry|
          entry_relative_path = File.join(dir, entry)
          out %{<File Id="F#{make_id(entry_relative_path)}" Source="#{@root}#{entry_relative_path.gsub('/', '\\')}" />}
        end
      end
    end 
  end

  def build_dir_tree files
    root = {}
    files.each do |file|
      components = file.gsub('/', '\\').split('\\')
      file_name = components.delete_at(-1)
      dir = components.reduce(root) { |node, component| node[component] ||= {} }
      (dir["."] ||= []) << file_name
    end
    root
  end

  def load_component_guids wxi_path
    if File.exists? wxi_path  
      @component_guids = {}
      File.foreach(wxi_path) do |line|
        if /Component Id="C([^"]+)".*Guid="([^"]+)"/ =~ line
          @component_guids[$1] = $2
        end
      end
    end
  end
  
  def make_component_guid id
    @component_guids[id] or System::Guid.new_guid
  end
  
  def write_component dir
    id = make_id(dir)
    out %{<Component Id="C#{id}" DiskId="1" Guid="#{make_component_guid(id)}">}
    indent
    yield
    unindent
    out %{</Component>}
  end
  
  def write_directory relative_path, name
    out %{<Directory Id="D#{make_id(relative_path)}" Name="#{name}">}
    indent
    yield
    unindent
    out %{</Directory>}
  end
  
  def make_id(path)
    # the hash needs to be short enough yet unique within a module to make msi happy:
    Digest::MD5.hexdigest(path).to_i(16).to_s(36)
  end
  
  def out str
    @out.print(' ' * @indent)
    @out.puts str
  end
  
  def indent
    @indent += 2
  end
  
  def unindent
    @indent -= 2
  end
  
  def file_name_without_extension path
    File.basename(path)[0..-File.extname(path).size-1]
  end
  
  def tf_edit path
    if File.exists?(path) and not File.writable?(path)
      puts tf = "tf edit #{path}"
      puts `#{tf}`
    end
  end
end